iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
生成式 AI

AI 產品與架構設計之旅:從 0 到 1,再到 Day 2系列 第 20

Day 20: 當 AI 也需要「翻譯蒟蒻」- 從提示詞到製作工具

  • 分享至 

  • xImage
  •  

嗨大家,我是 Debuguy。

今天要來聊一個很現實的話題:有時候 Prompt Engineering 真的不是萬能的

最一開始這個系列文章就有說到我希望的是之後會有 Slack AI Agent 來幫我找問題甚至解決問題
前面讓 Slack 可以像 ChatBot 已經花不少時間接下來
想說接個 Elasticsearch MCP 應該就能搞定 ... but!!!事情果然沒有想像中的簡單

接上 Elasticsearch MCP:看似簡單的開始

第一步:加上 Elasticsearch MCP 服務

想要讓 ChatBot 能查 Elasticsearch,第一件事就是把 Elasticsearch MCP 接上去。這步驟看起來很直觀:

# docker-compose.yml
services:
  elasticsearch-mcp:
    image: docker.elastic.co/mcp/elasticsearch
    environment:
      - ES_URL=${ES_URL}
      - ES_API_KEY=${ES_API_KEY}
    ports:
      - "8080:8080"
    command: ["http"]
    restart: unless-stopped

第二步:註冊到 MCP 配置

// config/mcp-config.json
{
  "mcpServers": {
    "elasticsearch": {
      "type": "streamable-http",
      "url": "http://elasticsearch-mcp:8080/mcp"
    }
  }
}

就這樣,理論上 ChatBot 就有了查詢 Elasticsearch 的能力了!

「看吧,很簡單嘛!現在只要告訴 AI 怎麼用就好了。」

但現實很快就告訴我事情沒這麼簡單...

美好的願景:讓 Bot 幫我們查 Log

原始構想很簡單

公司的 Alert 系統會發出 Kibana 的 URL,像這樣:

🚨 Error Alert 🚨
Production API 出現異常
查看詳細 Log: https://demo.elastic.co/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2025-10-04T14:30:00.000Z',to:'2025-10-04T15:00:00.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'filebeat-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'log.level:%20%22error%22%20'),sort:!(!('@timestamp',desc)))

@debuguy_bot 幫我看一下這次的錯誤 Log

我想要的自動化流程是:

  1. Alert 系統發通知(包含 @bot 觸發)
  2. Bot 自動解析 Kibana URL
  3. 直接查 Elasticsearch 回傳結果
  4. 團隊快速獲得分析結果

「這應該很簡單吧?不就是加個 Elasticsearch MCP 工具而已嗎?」

第一次嘗試

以為 System Prompt 可以搞定一切

一開始,我在 System Prompt 裡給了 URL pattern 和 DSL template:

## Kibana URL Pattern Recognition
When you see a URL like this:
https://demo.elastic.co/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2025-10-04T14:30:00.000Z',to:'2025-10-04T15:00:00.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'filebeat-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'log.level:%20%22error%22%20'),sort:!(!('@timestamp',desc)))

Generate Elasticsearch DSL using this template:
{
   "query":{
      "bool":{
         "filter":[
            {
               "bool":{
                  "should":[
                     {
                        "term":{
                           "log.level":{
                              "value":"error"
                           }
                        }
                     }
                  ]
               }
            },
            {
               "range":{
                  "@timestamp":{
                     "format":"strict_date_optional_time",
                     "gte":"2025-10-04T14:30:00.000Z",
                     "lte":"2025-10-04T15:00:00.000Z"
                  }
               }
            }
         ]
      }
   },
   "size":100
}

然後很期待地測試...
結果 AI 很常寫錯 query 語法

問題根源:工具本身的限制

深入研究後發現真相

我仔細看了 Elasticsearch MCP 的 JSON Schema:

{
  "name": "search",
  "inputSchema": {
    "properties": {
      "query_body": {
        "additionalProperties": true,
        "description": "Complete Elasticsearch query DSL object",
        "type": "object"
      }
    }
  }
}

發現問題了!

query_body 只是一個 "type": "object" 加上 "additionalProperties": true完全沒有結構化的 Schema!

LLM 要怎麼知道:

  • query_body 裡面應該放什麼?
  • Elasticsearch DSL 的正確格式是什麼?
  • 時間範圍應該怎麼寫?
  • KQL 要怎麼轉換成 DSL?

拆解 Kibana URL

讓我們來看看一個真實的 Kibana URL

https://demo.elastic.co/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2025-10-04T14:30:00.000Z',to:'2025-10-04T15:00:00.000Z'))&_a=(columns:!(),dataSource:(dataViewId:'filebeat-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'log.level:%20%22error%22%20'),sort:!(!('@timestamp',desc)))

看起來很嚇人對吧?讓我們一步步拆解:

Step 1:URL 結構

基礎 URL: https://demo.elastic.co/app/discover
Hash Fragment: #/?_g=...&_a=...

Kibana 把所有的狀態資訊都塞在 URL 的 hash fragment 裡面。

Step 2:參數分離

_g: 全域狀態 (Global State)
_a: 應用狀態 (App State)
  • _g 包含時間範圍、刷新間隔等全域設定
  • _a 包含查詢、過濾器、排序等應用特定設定

Step 3:Rison 格式解碼

這些參數使用一種叫做 Rison 的格式編碼,這是 JSON 的一種 URL-safe 變體:
這裡有個線上工具大家可以玩看看

// _g 的 Rison 編碼
(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:'2025-10-04T14:30:00.000Z',to:'2025-10-04T15:00:00.000Z'))

// _g 解碼後的 JSON
{
    "filters": [],
    "refreshInterval": {
        "pause": true,
        "value": 60000
    },
    "time": {
        "from": "2025-10-04T14:30:00.000Z",
        "to": "2025-10-04T15:00:00.000Z"
    }
}

// _a 的 Rison 編碼
(columns:!(),dataSource:(dataViewId:'filebeat-*',type:dataView),filters:!(),interval:auto,query:(language:kuery,query:'log.level:%20%22error%22%20'),sort:!(!('@timestamp',desc)))

// _a 解碼後的 JSON
{
    "columns": [],
    "dataSource": {
        "dataViewId": "filebeat-*",
        "type": "dataView"
    },
    "filters": [],
    "interval": "auto",
    "query": {
        "language": "kuery",
        "query": "log.level:%20%22error%22%20"
    },
    "sort": [
        [
            "@timestamp",
            "desc"
        ]
    ]
}

Step 5:_a 參數中的 kuery

// _a 解碼後
{
  "query": {
      "language": "kuery",
      "query": "log.level:%20%22error%22%20"
  },
}

這個 query 用 url decode 之後是
log.level: "error"

也就是 kurey: kibana 上的查詢語法

Step 6:KQL 到 Elasticsearch DSL

最複雜的部分是把 KQL (Kuery) 轉換成 Elasticsearch DSL:

// KQL
"log.level: \"error\""

// 需要轉換成 Elasticsearch DSL
   {
      "bool":{
         "filter":[
            {
               "bool":{
                  "should":[
                     {
                        "term":{
                           "log.level":{
                              "value":"error"
                           }
                        }
                     }
                  ]
               }
            }
         ]
      }
   }

完整的轉換流程

Kibana URL 
  ↓ 解析 hash fragment
URL Parameters (_g, _a)
  ↓ Rison decode
JSON Objects
  ↓ 提取時間範圍 (_g.time)
Time Range Filter
  ↓ 提取查詢條件 (_a.query)
KQL Query String
  ↓ KQL → DSL 轉換
Elasticsearch Query DSL
  ↓ 組合完整查詢
Final Query with filters, time range, sorting

這麼多步驟,每一步都有出錯的可能,難怪 LLM 搞不定!

務實的解決方案:該用 Code 的就用 Code

轉念:不要逼 AI 做它不擅長的事

我突然想通了:

與其花時間教 AI 怎麼轉換格式,不如直接寫個工具幫它轉換好。

這就像翻譯蒟蒻,讓它可以直接理解外星語言,而不是教它外星語的文法。

動手寫 Kibana URL 轉換工具

// GenKit/tools/kibana_tools.ts
export const createKibanaUrlToElasticQueryDslTool = (ai: Genkit) => {
  return ai.dynamicTool({
    name: 'kibana_url_to_elastic_query_dsl',
    description: 'Convert Kibana URL to Elasticsearch Query DSL. Parses Kibana discover URL and extracts query parameters, filters, and time range to generate corresponding Elasticsearch query.',
    inputSchema: z.object({
      kibanaUrl: z.string().describe('The Kibana discover URL containing query parameters')
    }),
    outputSchema: z.object({
      queryDsl: z.record(z.unknown()).describe('The generated Elasticsearch Query DSL object'),
      extractedParams: z.object({
        timeRange: z.object({
          from: z.string(),
          to: z.string()
        }).optional(),
        query: z.record(z.unknown()).optional(),
        filters: z.array(z.record(z.unknown())).optional()
      }),
      original: z.string().describe('The original Kibana URL')
    })
  }, async ({ kibanaUrl }) => {
    // 1. 解析 URL hash fragment
    const parsed = new URL(kibanaUrl);
    const hash = parsed.hash.substring(2); // 移除 '#/'
    const params = new URLSearchParams(hash);
    
    // 2. 解碼 rison 參數
    const _g = rison.decode(decodeURIComponent(params.get('_g')!));
    const _a = rison.decode(decodeURIComponent(params.get('_a')!));
    
    // 3. 建立時間範圍 filter
    const timeFilter = {
      range: {
        "@timestamp": {
          gte: _g.time.from,
          lte: _g.time.to,
          format: "strict_date_optional_time"
        }
      }
    };
    
    // 4. 使用 @vartoso/kql-to-dsl 轉換 KQL 到 DSL
    const queryDsl = buildEsQuery(undefined, _a.query, _a.filters, undefined);
    
    return {
      queryDsl: {
        query: queryDsl,
        size: 100,
        sort: [{ "@timestamp": { order: "asc" } }]
      },
      extractedParams: {
        timeRange: _g.time,
        query: _a.query,
        filters: _a.filters
      },
      original: parsed.pathname + parsed.hash
    };
  });
};

如果需要測試轉換的語法可以使用 Kibana 提供的 Dev Tools

關鍵技術:rison 和 KQL

Rison: Kibana 用來在 URL 中編碼 JSON 的格式,使用 rison-node 套件

KQL 轉 DSL: 使用 @vartoso/kql-to-dsl 套件

結果

新的工作流程

現在的流程變成:

User: 幫我查這個 Kibana URL 的資料
  ↓
Bot: 使用 kibana_url_to_elastic_query_dsl 工具
  ↓ 
工具自動轉換為正確的 Elasticsearch DSL
  ↓
Bot: 使用 elasticsearch search 工具查詢
  ↓
回傳查詢結果!

工具描述就是最好的 Prompt

最重要的是,工具的 description 本身就告訴了 LLM 該怎麼使用:

Convert Kibana URL to Elasticsearch Query DSL. Parses Kibana discover URL and extracts query parameters, filters, and time range to generate corresponding Elasticsearch query.

LLM 看到 Kibana URL 就知道:

  1. 我應該用這個工具
  2. 這個工具會幫我轉換格式
  3. 轉換後我就可以用 elasticsearch search 了

不需要複雜的 System Prompt,工具描述就夠了!

深刻的反思:該用 Code 的就用 Code

Prompt Engineering 的邊界

Prompt Engineering 很強大,但不是萬能的。當你發現自己在 Prompt 裡寫了太多「演算法」時,也許該考慮寫程式了。

適合 Prompt 的:

  • 邏輯推理和決策
  • 自然語言理解和生成
  • 創意和變化性任務

不適合 Prompt 的:

  • 精確的格式轉換
  • 複雜的數據處理
  • 需要 100% 正確的計算

工具設計的哲學

好的工具設計應該:

  1. 單一職責:一個工具做一件事,做到最好
  2. 明確描述:讓 LLM 一看就知道什麼時候該用
  3. 容錯處理:預期各種邊界情況並給出清楚的錯誤訊息
  4. 結構化輸出:提供一致且可預測的回傳格式

架構決策的影響

這個決定其實影響了整個系統的架構:

從「教 AI 做事」變成「給 AI 工具」

原本我想透過 Prompt 教 LLM 各種技能,現在我提供工具讓 LLM 呼叫專業服務。這樣的架構:

  • 更穩定:工具的行為是可預測的
  • 更易維護:工具可以獨立測試和更新
  • 更可擴展:新增功能就是新增工具

小結:找到 AI 和 Code 的最佳分工

最好的 AI 產品不是讓 AI 做所有事,而是讓 AI 和程式各自做最擅長的事。

AI 負責:

  • 理解使用者意圖
  • 決定使用哪些工具
  • 整合工具結果並回應

Code 負責:

  • 精確的資料轉換
  • 複雜的計算邏輯
  • 與外部系統整合

當我們放下「用 Prompt 解決一切」的執著,反而創造出更強大、更穩定的系統。


完整的原始碼在這裡


AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。

也歡迎追蹤我的 Threads @debuguy.dev


上一篇
Day 19: 一鍵直達 AI 大腦 - 在回覆中嵌入 Trace Link
下一篇
Day 21: 當警報響起時 - 從 Grafana Alert 到自動化問題追蹤
系列文
AI 產品與架構設計之旅:從 0 到 1,再到 Day 224
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言